package org.swing;

import com.jgoodies.validation.*;
import com.jgoodies.validation.view.ValidationComponentUtils;
import org.apache.log4j.Logger;
import org.swing.text.AllowBlankMaskFormatter;

import javax.swing.*;
import javax.swing.text.*;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.ParseException;
import java.util.Vector;

/**
 * Created by babitsky.viacheslav
 * Date: 24.01.2008
 * Time: 20:03:44
 */
public class JExtTextField extends JFormattedTextField implements Validatable {
    Logger logger = Logger.getLogger(JExtTextField.class);
    private AllowBlankMaskFormatter maskFormater = null;
    private IconFeedbackPanel iconFeedbackPanel;
    private Validator validator;
    private ValidationType validationType;
    private PropertyChangeListener propertyChangeListener;
    private int serialValue;
    private final Vector<ValidationMessage> validationMessageVector = new Vector<ValidationMessage>();
    private int maxLength = -1;

    /**
     * Constructs a new <code>TextField</code>.  A default model is created,
     * the initial string is <code>null</code>,
     * and the number of columns is set to 0.
     */
    public JExtTextField() {
        setFocusLostBehavior(JFormattedTextField.PERSIST);
        serialValue = ValidationComponentUtils.getNextSerialValue();
        ValidationComponentUtils.setMessageKey(this, serialValue);
        super.setDocument(new PlainDocument() {
            private static final long serialVersionUID = 1403942659333539445L;

            /**
             * Inserts some content into the document.
             * Inserting content causes a write lock to be held while the
             * actual changes are taking place, followed by notification
             * to the observers on the thread that grabbed the write lock.
             * <p/>
             * This method is thread safe, although most Swing methods
             * are not. Please see
             * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
             * to Use Threads</A> for more information.
             *
             * @param offs the starting offset >= 0
             * @param str  the string to insert; does nothing with null/empty strings
             * @param a    the attributes for the inserted content
             * @throws javax.swing.text.BadLocationException
             *          the given insert position is not a valid
             *          position within the document
             * @see javax.swing.text.Document#insertString
             */
            @Override
            public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
                if (maxLength >= 0) {
                    if (getLength() < maxLength) {
                        if (getLength() + str.length() > maxLength) {
                            str = str.substring(0, maxLength - getLength());
                        }
                    } else {
                        UIManager.getLookAndFeel().provideErrorFeedback(JExtTextField.this);
                        return;
                    }
                }
                super.insertString(offs, str, a);
            }

            /**
             * Deletes the region of text from <code>offset</code> to
             * <code>offset + length</code>, and replaces it with <code>text</code>.
             * It is up to the implementation as to how this is implemented, some
             * implementations may treat this as two distinct operations: a remove
             * followed by an insert, others may treat the replace as one atomic
             * operation.
             *
             * @param offset index of child element
             * @param length length of text to delete, may be 0 indicating don't
             *               delete anything
             * @param text   text to insert, <code>null</code> indicates no text to insert
             * @param attrs  AttributeSet indicating attributes of inserted text,
             *               <code>null</code>
             *               is legal, and typically treated as an empty attributeset,
             *               but exact interpretation is left to the subclass
             * @throws javax.swing.text.BadLocationException
             *          the given position is not a valid
             *          position within the document
             * @since 1.4
             */
            public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
                final String oldValue = getText(0, getLength());
                super.replace(offset, length, text, attrs);
                final String newValue = getText(0, getLength());
                firePropertyChange("value", oldValue, newValue);
            }

            /**
             * Removes some content from the document.
             * Removing content causes a write lock to be held while the
             * actual changes are taking place.  Observers are notified
             * of the change on the thread that called this method.
             * <p/>
             * This method is thread safe, although most Swing methods
             * are not. Please see
             * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
             * to Use Threads</A> for more information.
             *
             * @param offs the starting offset >= 0
             * @param len  the number of characters to remove >= 0
             * @throws javax.swing.text.BadLocationException
             *          the given remove position is not a valid
             *          position within the document
             * @see javax.swing.text.Document#remove
             */
            public void remove(int offs, int len) throws BadLocationException {
                final String oldValue = getText(0, getLength());
                super.remove(offs, len);
                final String newValue = getText(0, getLength());
                if (!newValue.equals("") || maskFormater == null) firePropertyChange("value", oldValue, newValue);
            }
        });
    }

    /**
     * <code>MaskFormatter</code> is used to format and edit strings. The behavior
     * of a <code>MaskFormatter</code> is controlled by way of a String mask
     * that specifies the valid characters that can be contained at a particular
     * location in the <code>Document</code> model. The following characters can
     * be specified:
     * <p/>
     * <table border=1 summary="Valid characters and their descriptions">
     * <tr>
     * <th>Character&nbsp;</th>
     * <th><p align="left">Description</p></th>
     * </tr>
     * <tr>
     * <td>#</td>
     * <td>Any valid number, uses <code>Character.isDigit</code>.</td>
     * </tr>
     * <tr>
     * <td>'</td>
     * <td>Escape character, used to escape any of the
     * special formatting characters.</td>
     * </tr>
     * <tr>
     * <td>U</td><td>Any character (<code>Character.isLetter</code>). All
     * lowercase letters are mapped to upper case.</td>
     * </tr>
     * <tr><td>L</td><td>Any character (<code>Character.isLetter</code>). All
     * upper case letters are mapped to lower case.</td>
     * </tr>
     * <tr><td>A</td><td>Any character or number (<code>Character.isLetter</code>
     * or <code>Character.isDigit</code>)</td>
     * </tr>
     * <tr><td>?</td><td>Any character
     * (<code>Character.isLetter</code>).</td>
     * </tr>
     * <tr><td>*</td><td>Anything.</td></tr>
     * <tr><td>H</td><td>Any hex character (0-9, a-f or A-F).</td></tr>
     * </table>
     * <p/>
     * <p/>
     * Typically characters correspond to one char, but in certain languages this
     * is not the case. The mask is on a per character basis, and will thus
     * adjust to fit as many chars as are needed.
     * <p/>
     * You can further restrict the characters that can be input by the
     * <code>setInvalidCharacters</code> and <code>setValidCharacters</code>
     * methods. <code>setInvalidCharacters</code> allows you to specify
     * which characters are not legal. <code>setValidCharacters</code> allows
     * you to specify which characters are valid. For example, the following
     * code block is equivalent to a mask of '0xHHH' with no invalid/valid
     * characters:
     * <pre>
     * MaskFormatter formatter = new MaskFormatter("0x***");
     * formatter.setValidCharacters("0123456789abcdefABCDEF");
     * </pre>
     * <p/>
     * When initially formatting a value if the length of the string is
     * less than the length of the mask, two things can happen. Either
     * the placeholder string will be used, or the placeholder character will
     * be used. Precedence is given to the placeholder string. For example:
     * <pre>
     *   MaskFormatter formatter = new MaskFormatter("###-####");
     *   formatter.setPlaceholderCharacter('_');
     *   formatter.getDisplayValue(tf, "123");
     * </pre>
     * <p/>
     * Would result in the string '123-____'. If
     * <code>setPlaceholder("555-1212")</code> was invoked '123-1212' would
     * result. The placeholder String is only used on the initial format,
     * on subsequent formats only the placeholder character will be used.
     * <p/>
     * If a <code>MaskFormatter</code> is configured to only allow valid characters
     * (<code>setAllowsInvalid(false)</code>) literal characters will be skipped as
     * necessary when editing. Consider a <code>MaskFormatter</code> with
     * the mask "###-####" and current value "555-1212". Using the right
     * arrow key to navigate through the field will result in (| indicates the
     * position of the caret):
     * <pre>
     *   |555-1212
     *   5|55-1212
     *   55|5-1212
     *   555-|1212
     *   555-1|212
     * </pre>
     * The '-' is a literal (non-editable) character, and is skipped.
     * <p/>
     * Similar behavior will result when editing. Consider inserting the string
     * '123-45' and '12345' into the <code>MaskFormatter</code> in the
     * previous example. Both inserts will result in the same String,
     * '123-45__'. When <code>MaskFormatter</code>
     * is processing the insert at character position 3 (the '-'), two things can
     * happen:
     * <ol>
     * <li>If the inserted character is '-', it is accepted.
     * <li>If the inserted character matches the mask for the next non-literal
     * character, it is accepted at the new location.
     * <li>Anything else results in an invalid edit
     * </ol>
     * <p/>
     * By default <code>MaskFormatter</code> will not allow invalid edits, you can
     * change this with the <code>setAllowsInvalid</code> method, and will
     * commit edits on valid edits (use the <code>setCommitsOnValidEdit</code> to
     * change this).
     * <p/>
     * By default, <code>MaskFormatter</code> is in overwrite mode. That is as
     * characters are typed a new character is not inserted, rather the character
     * at the current location is replaced with the newly typed character. You
     * can change this behavior by way of the method <code>setOverwriteMode</code>.
     * <p/>
     * <strong>Warning:</strong>
     * Serialized objects of this class will not be compatible with
     * future Swing releases. The current serialization support is
     * appropriate for short term storage or RMI between applications running
     * the same version of Swing.  As of 1.4, support for long term storage
     * of all JavaBeans<sup><font size="-2">TM</font></sup>
     * has been added to the <code>java.beans</code> package.
     * Please see {@link java.beans.XMLEncoder}.
     *
     * @version 1.13 11/17/05
     * @since 1.4
     */
    public void setMask(String maskFormater) {
        if (maskFormater == null) {
            this.maskFormater = null;
            setFormatterFactory(null);
            return;
        }
        try {
            this.maskFormater = new AllowBlankMaskFormatter(maskFormater);
        } catch (ParseException e) {
            logger.debug(e.getMessage(), e);
            throw new IllegalArgumentException(e.getMessage());
        }
        maxLength = -1;
        setFormatterFactory(new DefaultFormatterFactory(this.maskFormater));
    }

    public String getMask() {
        return maskFormater.getMask();
    }

    public int getMaxLength() {
        return maxLength;
    }

    public void setMaxLength(int maxLength) {
        this.maxLength = maxLength;
        if (maxLength >= 0) {
            setMask(null);
            String str = getText();
            if (str.length() > maxLength) setText(str.substring(0, maxLength));
        }
    }

    /**
     * @deprecated DO NOT USE THIS METHOD, use getText()
     */
    public Object getValue() {
        return super.getValue();
    }

    /**
     * @deprecated DO NOT USE THIS METHOD, use setText(java.lang.String)
     */
    public void setValue(Object value) {
        super.setValue(value);
    }


    /**
     * Associates the editor with a text document.
     * The currently registered factory is used to build a view for
     * the document, which gets displayed by the editor after revalidation.
     * A PropertyChange event ("document") is propagated to each listener.
     *
     * @param doc the document to display/edit
     * @beaninfo description: the text document model
     * bound: true
     * expert: true
     * @see #getDocument
     */
    public void setDocument(Document doc) {
    }

    /**
     * Processes any focus events, such as
     * <code>FocusEvent.FOCUS_GAINED</code> or
     * <code>FocusEvent.FOCUS_LOST</code>.
     *
     * @param e the <code>FocusEvent</code>
     * @see java.awt.event.FocusEvent
     */
    protected void processFocusEvent(FocusEvent e) {
        int caretPosition = getCaretPosition();
        super.processFocusEvent(e);
        setCaretPosition(caretPosition);
        if (e.getID() == FocusEvent.FOCUS_LOST) {
            if (validationType == validateOnLostFocus) {
                if (JExtTextField.this.getParent() != null) {
                    JExtTextField.this.iconFeedbackPanel.setOwnMessagesAndRevalidateOtherRegistredComponent(getSerialValue(), validateValue(), true);
                }
            }
        }
    }

    public void addValidater(IconFeedbackPanel iconFeedbackPanel, Validator validator, ValidationType validationType) {
        this.iconFeedbackPanel = iconFeedbackPanel;
        this.validator = validator;
        this.validationType = validationType;
        iconFeedbackPanel.registerComponent(this);
        if (validationType == validateOnValueChanged) {
            propertyChangeListener = new PropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent evt) {
                    if (JExtTextField.this.getParent() != null) {
                        JExtTextField.this.iconFeedbackPanel.setOwnMessagesAndRevalidateOtherRegistredComponent(getSerialValue(), validateValue(), false);
                    }
                }
            };
            addPropertyChangeListener("value", propertyChangeListener);
        }
    }

    public void removeValidater() {
        validator = null;
        validationType = null;
        if (propertyChangeListener != null) {
            removePropertyChangeListener(propertyChangeListener);
            propertyChangeListener = null;
        }
    }

    /**
     * Validates this object and returns a {@link com.jgoodies.validation.ValidationResult}.
     *
     * @return the validation result
     */
    public Vector<ValidationMessage> validateValue() {
        if (validator != null) {
            return validator.validate(getText(), serialValue);
        }
        return validationMessageVector;
    }

    public int getSerialValue() {
        return serialValue;
    }

    public void setMandarory(boolean mandatory) {
        ValidationComponentUtils.setMandatory(this, mandatory);
    }

    public boolean isMandatory() {
        return ValidationComponentUtils.isMandatory(this);
    }

    /**
     * Support for reporting bound property changes for Object properties.
     * This method can be called when a bound property has changed and it will
     * send the appropriate PropertyChangeEvent to any registered
     * PropertyChangeListeners.
     *
     * @param propertyName the property whose value has changed
     * @param oldValue     the property's previous value
     * @param newValue     the property's new value
     */
    @Override
    protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        if (newValue != null) super.firePropertyChange(propertyName, oldValue, newValue);
    }
}
